/**
 * Copyright © 2025 650 Industries.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

const getHeadersConstructor = (): typeof Headers => {
  if (typeof Headers !== 'undefined') {
    return Headers;
  } else {
    // NOTE(@kitten): The `assertRuntimeFetchAPISupport` helper will catch this. Currently only an issue in Jest
    return (
      globalThis.Headers ??
      class _MockHeaders {
        constructor() {
          throw new Error('Runtime built-in Headers API is not available.');
        }
      }
    );
  }
};

/**
 * An immutable version of the Fetch API's [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object which prevents mutations.
 */
class ImmutableHeaders extends getHeadersConstructor() {
  // TODO(@hassankhan): Merge with `ReadonlyHeaders` from `expo-router`
  #throwImmutableError() {
    throw new Error('This operation is not allowed on immutable headers.');
  }

  set() {
    this.#throwImmutableError();
  }
  append() {
    this.#throwImmutableError();
  }
  delete() {
    this.#throwImmutableError();
  }
}

/** @hidden */
export type _ImmutableRequest = Omit<
  Request,
  'body' | 'bodyUsed' | 'arrayBuffer' | 'blob' | 'formData' | 'json' | 'text' | 'bytes' | 'headers'
> & {
  headers: ImmutableHeaders;
};

/**
 * An immutable version of the Fetch API's [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object which prevents mutations to the request body and headers.
 */
export class ImmutableRequest implements _ImmutableRequest, RequestInit {
  readonly #headers: ImmutableHeaders;
  readonly #request: Request;

  constructor(request: Request) {
    this.#headers = new ImmutableHeaders(request.headers);
    this.#request = request;
  }

  get cache() {
    return this.#request.cache;
  }

  get credentials() {
    return this.#request.credentials;
  }

  get destination() {
    return this.#request.destination;
  }

  get integrity() {
    return this.#request.integrity;
  }

  get keepalive() {
    return this.#request.keepalive;
  }

  get method() {
    return this.#request.method;
  }

  get mode() {
    return this.#request.mode;
  }

  get redirect() {
    return this.#request.redirect;
  }

  get referrer() {
    return this.#request.referrer;
  }

  get referrerPolicy() {
    return this.#request.referrerPolicy;
  }

  get signal() {
    return this.#request.signal;
  }

  get url() {
    return this.#request.url;
  }

  get bodyUsed() {
    return this.#request.bodyUsed;
  }

  get duplex() {
    return this.#request.duplex;
  }

  get headers(): ImmutableHeaders {
    return this.#headers;
  }

  #throwImmutableBodyError() {
    throw new Error('This operation is not allowed on immutable requests.');
  }

  /** The request body is not accessible in immutable requests. */
  get body(): never {
    // NOTE(@kitten): `new Request(req.url, req)` may internally access `req.body` to copy the request
    // We can pretend it is `null`. Marking `bodyUsed` makes no sense here as it manipulates the subsequent
    // code paths, but pretending there was no body should be safe
    return null as never;
  }

  async arrayBuffer() {
    this.#throwImmutableBodyError();
  }

  async blob() {
    this.#throwImmutableBodyError();
  }

  async bytes() {
    this.#throwImmutableBodyError();
  }

  async formData() {
    this.#throwImmutableBodyError();
  }

  async json() {
    this.#throwImmutableBodyError();
  }

  async text() {
    this.#throwImmutableBodyError();
  }

  /**
   * Creates a mutable clone of the original request. This is provided as an escape hatch.
   */
  clone(): Request {
    return this.#request.clone();
  }
}

// Add assertions to improve usage in non-standard environments.
export function assertRuntimeFetchAPISupport({
  Request,
  Response,
  Headers,
  process,
}: any = globalThis) {
  // Check if Request and Response are available.
  if (
    typeof Request === 'undefined' ||
    typeof Response === 'undefined' ||
    typeof Headers === 'undefined'
  ) {
    // Detect if `--no-experimental-fetch` flag is enabled and warn that it must be disabled.
    if (typeof process !== 'undefined' && process.env && process.env.NODE_OPTIONS) {
      const nodeOptions = process.env.NODE_OPTIONS;
      if (nodeOptions.includes('--no-experimental-fetch')) {
        throw new Error(
          'NODE_OPTIONS="--no-experimental-fetch" is not supported with Expo server. Node.js built-in Request/Response APIs are required to continue.'
        );
      }
    }
    // If Node.js is <18, throw an error.
    if (typeof process !== 'undefined' && process.version) {
      const version = process.version;
      const majorVersion = parseInt(version.replace(/v/g, '').split('.')[0], 10);
      if (majorVersion < 18) {
        throw new Error(
          `Node.js version ${majorVersion} is not supported. Upgrade to Node.js 20 or newer.`
        );
      }
    }

    // Default error event for missing APIs.
    throw new Error(
      'Runtime built-in Request/Response/Headers APIs are not available. If running Node ensure that Node Fetch API, first available in Node.js 18, is enabled.'
    );
  }
}
